package tms.model;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.TreeMap;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import tms.utils.XMLHelper;
import tms.utils.XMLHelper.XsdValidationException;

/**
 * The TaskTree class is a singleton representing the tasks in the TMS
 * It extends AbstractModelObject and implements PropertyChangeListener
 * to allow the fire of property changes
 * 
 * @author Amer Alhalabi/Rolf Schmidt/Dmitriy Fingerman
 * @version 2.0
 * 
 */
public class TaskTree extends AbstractModelObject implements PropertyChangeListener {

	/**
	 * header string for the csv export of the tasks
	 */
	private static final String HEADER_STRING = "ID,Title,Description,Duration,Deliverable,Deadline,Person\n";

	/**
	 * the only instance of the class
	 */
	private static TaskTree s_instance = null;

	/**
	 * holds all Task objects (taskId, Task)
	 */
	private TreeMap<Integer, Task> taskMap = new TreeMap<Integer, Task>();

	/**
	 * a list of tasks that represents the taskTree (as the individual tasks
	 * contain subtasks)
	 */
	private ArrayList<Task> taskList = new ArrayList<Task>();

	/**
	 * a helper to validate and parse input files
	 */
	private XMLHelper xmlHelper;

	/**
	 * getter of the only instance of the class
	 * 
	 * @return the only instance of the class
	 */
	public static TaskTree getInstance() {
		if (s_instance == null)
			s_instance = new TaskTree();
		return s_instance;
	}

	/**
	 * private constructor to prevent the creation the multiple instances of the
	 * class
	 */
	private TaskTree() {
		initializeXMLHelper();
	}

	/**
	 * @return TreeMap of tasks
	 */

	public TreeMap<Integer, Task> getTaskMap() {
		return taskMap;
	}

	/**
	 * @return list of tasks (i.e. the taskTree)
	 */
	public ArrayList<Task> getTasklist() {
		return taskList;
	}

	/**
	 * @param taskList
	 *            the taskTree to set
	 */
	public void setTaskList(ArrayList<Task> taskList) {
		List<Task> oldValue = this.taskList;
		this.taskList = taskList;
		firePropertyChange("taskTree", null, oldValue);
	}

	/**
	 * clear the taskList
	 */
	public void clear() {
		taskList.clear();
		taskMap.clear();
		firePropertyChange("taskTree", null, this);
	}

	/**
	 * Add a task to the taskList.
	 * If the task to be added is null or already exists, then it will not be
	 * added
	 * A tasks change property event will be fired if the addition of the task
	 * is successful
	 * 
	 * @param task
	 *            the task to add
	 * @return the added task
	 */
	public Task addTask() {
		Task task = new Task();
		setUniqueId(task);
		taskMap.put(task.getId(), task);
		taskList.add(task);
		task.addPropertyChangeListener(this);
		firePropertyChange("taskTree", null, this);

		return task;
	}

	/**
	 * Add a subtask to parent.
	 * A tasks change property event will be fired if the addition of the task
	 * is successful
	 * 
	 * @param parent
	 *            the task to which the subtask will be added
	 * @return the added subtask
	 */
	public Task addSubtask(Task parent) {
		
		if (parent == null)
			return null;
		
		if (!this.taskMap.containsKey(parent.getId()))
			return null;
		
		if (!taskMap.containsKey(parent.getId()))
			return null;
		
		Task subtask = new Task();
		setUniqueId(subtask);
		taskMap.put(subtask.getId(), subtask);
		parent.addSubtask(subtask);
		subtask.addPropertyChangeListener(this);
		firePropertyChange("taskTree", null, this);

		return subtask;
	}

	/**
	 * remove a task from the taskList
	 * If the task to be removed is null or does not exist, then it will not be
	 * removed
	 * A tasks change property event will be fired if the removal of the task is
	 * successful
	 * 
	 * @param task
	 *            the task to remove
	 */
	public void removeTask(Task task) {
		if (task != null) {
			// if 'task' is a subtask, its parent is responsible for removing it
			if (task.hasParent()) {
				Task parent = task.getParent();
				parent.removeSubtask(task);
			} else {
				// a task can be removed directly
				taskList.remove(task);
			}
			taskMap.remove(task.getId());
			firePropertyChange("taskTree", null, this);
		}
	}

	/**
	 * helper function to process the XML input file
	 * 
	 * @param node
	 *            the node to process
	 * @param parent
	 *            the parent task
	 *
	 * @return true if the subtasks were processed successfully, false otherwise
	 */
	private boolean loadSubTasksFromXML(Node node, Task parent) {
		try {
			Iterable<Node> subtasks = xmlHelper.xPathNodes("subtasks/subtask", node);
			for (Node subtask : subtasks) {
				Task task = getTask(subtask);

				// if a subtask has a beforeId other than -1, the file is invalid
				int beforeId = task.getBeforeId();
				if (beforeId != -1) {
					System.out.println("Invalid file content - before: " + beforeId + " already exists.");
					System.out.println("loadSubTasksFromXML() aborted.");
					return false;
				}
				// if the task id already exists, the file is invalid
				int taskId = task.getId();
				if (taskMap.containsKey(taskId)) {
					System.out.println("Invalid file content - task: " + taskId + " already exists.");
					System.out.println("loadSubTasksFromXML() aborted.");
					return false;
				}

				taskMap.put(taskId, task);
				parent.addSubtask(task);

				// recursively load all subtasks
				if (!loadSubTasksFromXML(subtask, task)) {
					return false;
				}
			}
		} catch (Exception e) {
			System.out.println("Exception in loadSubTasksFromXML()");
			System.out.println(e.getMessage());
			e.printStackTrace();
			return false;
		}
		return true;
	}

	/*
	 * loadXMLFile populates the taskTree instance with tasks that are read
	 * from an input file.
	 * If the filename is invalid (empty string, null, or does not exist) or if
	 * the format and content of the file is invalid, no tasks will be added to
	 * the taskList and false will be returned.
	 * 
	 * 
	 * @param filename
	 * the path to the input file that contains the tasks to be
	 * populated in the system
	 */
	public boolean loadXMLFile(String fileName) {
		if (fileName == null || fileName == "") {
			System.out.println("Invalid file name.");
			return false;
		}
			if (xmlHelper != null) {
		try {
				taskMap.clear();
				taskList.clear();
				if (!validateXMLFile(fileName)) {
					return false;
				}
				// load the XML file into a string
				FileInputStream xmlInputStream;
				xmlInputStream = new FileInputStream(new File(fileName));
				String xmlInputString = XMLHelper.streamToString(xmlInputStream);

				Document doc = xmlHelper.getDocument(xmlInputString);

				Node root = doc.getDocumentElement();
				Iterable<Node> tasks = xmlHelper.xPathNodes("task", root);
				// for each element in the task, get the task attributes and
				// validate them if the validation is successful, add the task
				// to the taskList and add a property change listener to the
				// task; the listener is the taskList
				for (Node node : tasks) {
					Task task = getTask(node);
					int taskId = task.getId();
					if (taskMap.containsKey(taskId)) {
						taskMap.clear();
						taskList.clear();
						System.out.println("Invalid file content - task: " + taskId + " already exists.");
						System.out.println("loadXMLFile() aborted.");
						return false;
					}
					taskMap.put(taskId, task);
					taskList.add(task);
					task.addPropertyChangeListener(this);

					// load the subtasks of this task
					if (!loadSubTasksFromXML(node, task)) {
						taskMap.clear();
						taskList.clear();
						return false;
					}
				}
				// if there is a beforeId that does not have a corresponding
				// task id the file load fails
				if (!validateBeforeIds()) {
					taskMap.clear();
					taskList.clear();
					return false;
				}
				firePropertyChange("taskTree", null, this);
				return true;
		} catch (FileNotFoundException e) {
			System.out.println("FileNotFoundException in loadXMLFile()");
			System.out.println(e.getMessage());
			e.printStackTrace();
			return false;
		} catch (IOException e) {
			System.out.println("IOException in loadXMLFile()");
			System.out.println(e.getMessage());
			e.printStackTrace();
			return false;
		} catch (ParseException e) {
			System.out.println("ParseException in loadXMLFile()");
			System.out.println(e.getMessage());
			e.printStackTrace();
			return false;
		}
			} else {
				System.out.println("XMLHelper not initialised.");
				System.out.println("loadXMLFile() aborted.");
				return false;
			}
	}

	/**
	 * check if all beforeIds are valid
	 * 
	 * @return true if the beforeIds are valid, false otherwise
	 */
	private boolean validateBeforeIds() {
		// check that every beforeID != -1 has a corresponding task in the project
		for (Task task : taskList) {
			int beforeID = task.getBeforeId();
			if (beforeID != -1) {
				if (!taskMap.containsKey(beforeID)) {
					System.out.println("Invalid file content - beforeID: " + beforeID + " of task " + task.getId() + " does not have a corresponding task.");
					System.out.println("validateBeforeIds() aborted.");
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * initialises the XMLHelper for validation and parsing
	 * 
	 * @return the XMLHelper
	 */
	private XMLHelper initializeXMLHelper() {
		try {
			// initialise the XMLHelper with the schema file
			InputStream xsdInputStream = getClass().getResourceAsStream("/comp5541.xsd");
			String xsdInputString = XMLHelper.streamToString(xsdInputStream);
			xmlHelper = new XMLHelper(xsdInputString);
		} catch (FileNotFoundException e) {
			System.out.println("XsdValidationException in validateXMLFile()");
			System.out.println(e.getMessage());
			e.printStackTrace();
			return null;
		} catch (IOException e) {
			System.out.println("IOException in getXMLHelper()");
			System.out.println(e.getMessage());
			e.printStackTrace();
			return null;
		}
		return xmlHelper;
	}

	/**
	 * validates an XML file
	 * 
	 * @return true if the XML file is valid, false otherwise
	 */
	private boolean validateXMLFile(String fileName) {
		try {
			// load an XML file into the XMLHelper
			File xmlFile = new File(fileName);
			FileInputStream xmlInputStream = new FileInputStream(xmlFile);
			String xmlInputString = XMLHelper.streamToString(xmlInputStream);
			// validate the XML file against the XSD schema
			Document doc = xmlHelper.getDocument(xmlInputString);
		    xmlHelper.validate(doc);
		} catch (ParseException e) {
			System.out.println("ParseException in validateXMLFile()");
			System.out.println(e.getMessage());
			e.printStackTrace();
			return false;
		} catch (XsdValidationException e) {
			System.out.println("XsdValidationException in validateXMLFile()");
			System.out.println(e.getMessage());
			e.printStackTrace();
			return false;
		} catch (FileNotFoundException e) {
			System.out.println("FileNotFoundException in validateXMLFile()");
			System.out.println(e.getMessage());
			e.printStackTrace();
			return false;
		} catch (IOException e) {
			System.out.println("IOException in validateXMLFile()");
			System.out.println(e.getMessage());
			e.printStackTrace();
			return false;
		}
		return true;
	}

	/**
	 * method to access a task with a given id
	 * 
	 * @return the Task with the corresponding id
	 */
	public Task getTask(int id) {
		if (taskMap.containsKey(id)) {
			return taskMap.get(id);
		}
		return null;
	}

	/**
	 * method to create a Task from an XML node
	 * 
	 * @return the Task with the corresponding id
	 */
	private Task getTask(Node node) {
		Calendar cal = Calendar.getInstance();

		SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
		try {
			String deadline = xmlHelper.xPathString("deadline", node);
			cal.setTime(simpleDateFormat.parse(deadline));
		} catch (ParseException e) {
			System.out.println("Deadline could not be converted.");
		}
		// extract the property values using XPath expressions
		Task task = new Task();
		task.setId(Integer.parseInt(xmlHelper.xPathString("id", node)));
		task.setTitle(xmlHelper.xPathString("title", node));
		task.setDescription(xmlHelper.xPathString("description", node));
		task.setDuration(Integer.parseInt(xmlHelper.xPathString("duration", node)));
		task.setDeliverable(xmlHelper.xPathString("deliverable", node));
		task.setDeadline(cal.getTime());
		task.setBeforeId(Integer.parseInt(xmlHelper.xPathString("before", node)));
		task.setPerson(xmlHelper.xPathString("person", node));

		return task;
	}

	/**
	 * find and set unique ID
	 */
	public void setUniqueId(Task task) {
		Integer id = 0;
		// increase the id until it is unique (i.e. new/unused)
		while (taskMap.containsKey(id)) {
			id++;
		}
		task.setId(id);
	}

	public Boolean saveCSVFile(String fileName) {
		// do not allow the filename to be empty or null
		if (fileName == null || fileName == "") {
			System.out.println("Invalid file name.");
			return false;
		}
		PrintWriter printOut = null;
		// add the header to the StringBuilder that will be written to the
		// CSV file
		StringBuilder sb = new StringBuilder(HEADER_STRING);

		try {
			printOut = new PrintWriter(fileName);
			// loop over the task objects and retrieve its attribute
			// separate each attribute with a comma and add that to a
			// StringBuilder, which will written to the CSV file
			for (Task task : taskList) {
				sb.append(task.getId());
				sb.append(",");
				sb.append(task.getTitle());
				sb.append(",");
				sb.append(task.getDescription());
				sb.append(",");
				sb.append(task.getDuration());
				sb.append(",");
				sb.append(task.getDeliverable());
				sb.append(",");
				sb.append(task.getDeadline());
				sb.append(",");
				sb.append(task.getPerson());
				sb.append("\n");
			}
			printOut.print(sb);
		} catch (FileNotFoundException e) {
			System.out.println("File: " + fileName);
			System.out.println(e.getMessage());
			return false;
		} finally {
			printOut.close();
		}
		return true;
	}

	/**
	 * @return the top-level tasks in a list (i.e. subtasks are excluded)
	 */
	public ArrayList<Task> getTopLevelTasklist() {
		ArrayList<Task> topLevelTasklist = new ArrayList<Task>();

		for (Task task : taskList) {
			topLevelTasklist.add(task);
		}
		return topLevelTasklist;
	}

	/**
	 * @return the tasks and subtasks in a flat list (i.e. no hierarchy)
	 */
	public ArrayList<Task> getFlatTasklist() {
		ArrayList<Task> flatTaskList = new ArrayList<Task>();

		for (Task task : taskList) {
			flatTaskList.add(task);
			for (Task subtask : task.getSubtasks()) {
				processSubtasks(subtask, flatTaskList);
			}
		}
		return flatTaskList;
	}

	/**
	 * helper method for getFlatTasklist() to do the recursion
	 */
	private void processSubtasks(Task subtask, ArrayList<Task> flatTaskList) {
		flatTaskList.add(subtask);
		for (Task task : subtask.getSubtasks()) {
			processSubtasks(task, flatTaskList);
		}
	}

	/**
	 * helper method for saveXMLFile() to save task and its sub-tasks in
	 * recursion
	 */
	private void taskDataToXML(Document doc, Task t, Element parentElem, String task_hierarchy_type) {

		DateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd");

		// task / sub-task element
		Element taskElem = doc.createElement(task_hierarchy_type);
		parentElem.appendChild(taskElem);

		// convert the task members to XML nodes
		Element idElem = doc.createElement("id");
		idElem.appendChild(doc.createTextNode(Integer.toString(t.getId())));
		taskElem.appendChild(idElem);

		Element titleElem = doc.createElement("title");
		titleElem.appendChild(doc.createTextNode(t.getTitle()));
		taskElem.appendChild(titleElem);

		Element descElem = doc.createElement("description");
		descElem.appendChild(doc.createTextNode(t.getDescription()));
		taskElem.appendChild(descElem);

		Element durationElem = doc.createElement("duration");
		durationElem.appendChild(doc.createTextNode(Integer.toString(t.getDuration())));
		taskElem.appendChild(durationElem);

		Element deadlineElem = doc.createElement("deadline");
		deadlineElem.appendChild(doc.createTextNode(dateFormatter.format(t.getDeadline())));
		taskElem.appendChild(deadlineElem);

		Element deliverableElem = doc.createElement("deliverable");
		deliverableElem.appendChild(doc.createTextNode(t.getDeliverable()));
		taskElem.appendChild(deliverableElem);

		Element personElem = doc.createElement("person");
		personElem.appendChild(doc.createTextNode(t.getPerson()));
		taskElem.appendChild(personElem);

		Element beforeTaskIdElem = doc.createElement("before");
		beforeTaskIdElem.appendChild(doc.createTextNode(Integer.toString(t.getBeforeId())));
		taskElem.appendChild(beforeTaskIdElem);

		if (!t.getSubtasks().isEmpty()) {
			Element subtasksElem = doc.createElement("subtasks");
			taskElem.appendChild(subtasksElem);

			for (Task subTask : t.getSubtasks()) {
				taskDataToXML(doc, subTask, subtasksElem, "subtask");
			}
		}
	}

	/**
	 * method to save project data to output file in XML format
	 * 
	 * @param fileName
	 *            output XML file name
	 * @return boolean
	 */

	public Boolean saveXMLFile(String fileName) {
		// do not allow the filename to be empty or null
		if (fileName == null || fileName == "") {
			System.out.println("Invalid file name.");
			return false;
		}
		try {
			Document doc = xmlHelper.newDocument();

			// root element
			Element rootElem = doc.createElement("data");
			doc.appendChild(rootElem);
			// convert each Task to an XML node
			for (Task task : taskList) {
				taskDataToXML(doc, task, rootElem, "task");
			}
			// write the content into an xml file
			Transformer transformer = xmlHelper.newTransformer();
			DOMSource source = new DOMSource(doc);
			StreamResult result = new StreamResult(new File(fileName));
			// pretty print and create file
			transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
			transformer.setOutputProperty(OutputKeys.INDENT, "yes");
			transformer.transform(source, result);
		} catch (TransformerConfigurationException e) {
			e.printStackTrace();
		} catch (TransformerException e) {
			e.printStackTrace();
		}
		return true;
	}

	@Override
	/**
	 * this method gets called whenever there is a property change
	 * 
	 * @param evt a PropertyChangeEvent object describing the event source and the property that has changed.
	 */
	public void propertyChange(PropertyChangeEvent evt) {
		firePropertyChange("taskTree", null, this);
	}

}